package com.hmdp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.enums.ErrorCodeEnum;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;

import java.time.LocalDateTime;

/**
 * 秒杀抢购优惠券服务实现类
 *
 * @author leizi
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    private final SeckillVoucherServiceImpl seckillVoucherService;

    private final RedisIdWorker redisIdWorker;

    public VoucherOrderServiceImpl(SeckillVoucherServiceImpl seckillVoucherService, RedisIdWorker redisIdWorker) {
        this.seckillVoucherService = seckillVoucherService;
        this.redisIdWorker = redisIdWorker;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result seckillVoucher(Long voucherId) {

        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        if (ObjectUtils.isEmpty(seckillVoucher)) {
            return Result.fail(ErrorCodeEnum.VOUCHER_SOLD_OUT);
        }
        // 检查优惠券时效
        LocalDateTime currentTime = LocalDateTime.now();
        // 未到开始时间
        if (seckillVoucher.getBeginTime().isAfter(currentTime)) {
            return Result.fail(ErrorCodeEnum.VOUCHER_NOT_START_TIME);
        }
        // 抢购时间已过期
        if (seckillVoucher.getEndTime().isBefore(currentTime)) {
            return Result.fail(ErrorCodeEnum.VOUCHER_NOT_END_TIME);
        }

        // 检查库存量
        if (seckillVoucher.getStock() < 1) {
            return Result.fail(ErrorCodeEnum.VOUCHER_STOCK_EMPTY);
        }

        // 校验用户是否已经生成订单
        Long userId = UserHolder.getUser().getId();
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            return Result.fail(ErrorCodeEnum.VOUCHER_SOLD_REPEAT);
        }

        // 减库存
        /*
         * 这里依然存在并发减库存超卖问题，因为修改字段值方式仍然有并发减字段值，再更新表，而且使用的是stock库存量大于0的条件
         * 所以，多个线程可能获取到相同的库存量并且库存都减1再更新表，导致一个库存减1后被更新多次，实际库存只减了一次却生成了多个订单
         * 解决方式：减库存量在数据库中操作，不能在代码中修改字段值
         * */
//        seckillVoucher.setStock(seckillVoucher.getStock() - 1);
//        UpdateWrapper<SeckillVoucher> updateWrapper = new UpdateWrapper<>();
//        updateWrapper.set("stock", seckillVoucher.getStock() - 1)
//                .eq("voucher_id", voucherId)
//                .gt("stock", 0);
//        boolean reduceResult = seckillVoucherService.update(updateWrapper);

        /*
         * 使用乐观锁解决超卖问题
         * 以库存量与版本号需要同时递减，所以直接以库存量作为版本号控制，减库存的条件是库存量与表中库存量相同则执行
         * 乐观锁问题：
         * 以库存量为版本号进行比较，此方式虽然解决了超卖问题，但是不同线程可能会获取到相同的库存量，减库存时比较库存量值是否相同会造成
         * 部分线程减库存失败，秒杀抢购的失败率上升，实际还有很多库存
         * 解决方案：
         * 不以库存值作为限制，只要有库存且库存量大于0则可以被抢购，降低抢购失败率
         * */
        boolean reduceResult = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();
        if (!reduceResult) {
            return Result.fail(ErrorCodeEnum.VOUCHER_STOCK_EMPTY);
        }

        // 添加秒杀优惠券订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 订单id(全局唯一id)
        voucherOrder.setId(redisIdWorker.nextId("voucher"));
        voucherOrder.setVoucherId(voucherId);
        voucherOrder.setUserId(userId);
        save(voucherOrder);

        // 返回订单id
        return Result.ok(voucherOrder.getId());
    }
}
